<!DOCTYPE html>
<title>Tests which nodes are upgraded after adding a scoped custom element definition</title>
<meta name="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
<link rel="help" href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries">
<link rel="help" href="https://github.com/WICG/webcomponents/issues/923">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<body>

<script>
function attachShadowForTest(t, registry) {
  const host = document.createElement('div');
  const shadow = host.attachShadow({mode: 'open', registry});
  document.body.appendChild(host);
  t.add_cleanup(() => host.remove());
  return shadow;
}

function createIFrameForTest(t) {
  const iframe = document.createElement('iframe');
  document.body.appendChild(iframe);
  if (!iframe.contentDocument.body) {
    iframe.contentDocument.body = iframe.contentDocument.createElement('body');
  }
  t.add_cleanup(() => iframe.remove());
  return iframe;
}

let definitionCount = 0;
function nextCustomElementName() {
  return `test-element-${++definitionCount}`;
}

test(t => {
  const name = nextCustomElementName();
  document.body.appendChild(document.createElement(name));

  const registry = new CustomElementRegistry;
  const shadow = attachShadowForTest(t, registry);
  shadow.appendChild(document.createElement(name));

  class TestElement extends HTMLElement {};
  customElements.define(name, TestElement);

  assert_true(document.querySelector(name) instanceof TestElement);
  assert_false(shadow.querySelector(name) instanceof TestElement);
}, 'Adding definition to global registry should not affect shadow roots using scoped registry');

test(t => {
  const name = nextCustomElementName();

  const shadow1 = attachShadowForTest(t, customElements);
  shadow1.appendChild(document.createElement(name));

  const shadow2 = attachShadowForTest(t);
  shadow2.appendChild(document.createElement(name));

  class TestElement extends HTMLElement {};
  customElements.define(name, TestElement);

  assert_true(shadow1.querySelector(name) instanceof TestElement);
  assert_true(shadow2.querySelector(name) instanceof TestElement);
}, 'Adding definition to global registry should affect shadow roots also using global registry');

test(t => {
  const name = nextCustomElementName();

  const registry = new CustomElementRegistry;
  const shadow1 = attachShadowForTest(t, registry);
  shadow1.appendChild(document.createElement(name));

  const shadow2 = attachShadowForTest(t, registry);
  shadow2.appendChild(document.createElement(name));

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_true(shadow1.querySelector(name) instanceof TestElement);
  assert_true(shadow2.querySelector(name) instanceof TestElement);
}, 'Adding definition to scoped registry should affect all associated shadow roots');

test(t => {
  const name = nextCustomElementName();
  document.body.appendChild(document.createElement(name));

  const registry = new CustomElementRegistry;
  const shadow = attachShadowForTest(t, registry);
  shadow.appendChild(document.createElement(name));

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_false(document.querySelector(name) instanceof TestElement);
  assert_true(shadow.querySelector(name) instanceof TestElement);
}, 'Adding definition to scoped registry should not affect document tree scope');

test(t => {
  const name = nextCustomElementName();

  const registry = new CustomElementRegistry;
  const shadow1 = attachShadowForTest(t, registry);
  shadow1.appendChild(document.createElement(name));

  const shadow2 = attachShadowForTest(t, new CustomElementRegistry);
  shadow2.appendChild(document.createElement(name));

  const shadow3 = attachShadowForTest(t);
  shadow3.appendChild(document.createElement(name));

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_true(shadow1.querySelector(name) instanceof TestElement);
  assert_false(shadow2.querySelector(name) instanceof TestElement);
  assert_false(shadow3.querySelector(name) instanceof TestElement);
}, 'Adding definition to scoped registry should not affect shadow roots using other registries');

test(t => {
  const name = nextCustomElementName();
  const node = document.body.appendChild(document.createElement(name));

  const registry = new CustomElementRegistry;
  const shadow = attachShadowForTest(t, registry);
  shadow.appendChild(node);

  class TestElement extends HTMLElement {};
  customElements.define(name, TestElement);

  assert_false(node instanceof TestElement);
}, 'Adding definition to global registry should not upgrade nodes no longer using the registry');

test(t => {
  const name = nextCustomElementName();

  const registry = new CustomElementRegistry;
  const shadow1 = attachShadowForTest(t, registry);
  const node = shadow1.appendChild(document.createElement(name));

  const shadow2 = attachShadowForTest(t, new CustomElementRegistry);
  shadow2.appendChild(node);

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_false(node instanceof TestElement);
}, 'Adding definition to scoped registry should not upgrade nodes no longer using the registry');

test(t => {
  const name = nextCustomElementName();

  const registry = new CustomElementRegistry;
  const shadow1 = attachShadowForTest(t, registry);
  const node1 = shadow1.appendChild(document.createElement(name));

  const iframe = createIFrameForTest(t);
  const host2 = iframe.contentDocument.createElement('div');
  const shadow2 = host2.attachShadow({mode: 'open', registry});
  const node2 = shadow2.appendChild(iframe.contentDocument.createElement(name));
  iframe.contentDocument.body.appendChild(host2);

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_true(node1 instanceof TestElement);
  assert_true(node2 instanceof TestElement);
}, 'Adding definition to scoped registry affects associated shadow roots in all iframes');

test(t => {
  const name = nextCustomElementName();

  const newWindow = window.open('about:blank');
  t.add_cleanup(() => newWindow.close());

  const host = newWindow.document.createElement('div');
  const shadow = host.attachShadow({mode: 'open', registry: window.customElements});
  const node = shadow.appendChild(newWindow.document.createElement(name));
  newWindow.document.body.appendChild(host);

  class TestElement extends HTMLElement {};
  window.customElements.define(name, TestElement);

  assert_true(node instanceof TestElement);
}, 'Adding definition to scoped registry affects associated shadow roots in other frame trees');

test(t => {
  const name = nextCustomElementName();

  const registry = new CustomElementRegistry;
  const shadow = attachShadowForTest(t, registry);
  const node = shadow.appendChild(document.createElement(name));
  shadow.host.remove();

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_false(node instanceof TestElement);
}, 'Adding definition to scoped registry should not upgrade disconnected elements');

test(t => {
  const name = nextCustomElementName();

  const registry = new CustomElementRegistry;
  const doc = document.implementation.createHTMLDocument();
  const host = doc.createElement('div');
  const shadow = host.attachShadow({mode: 'open', registry});
  const node = shadow.appendChild(doc.createElement(name));
  doc.body.appendChild(host);

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_false(node instanceof TestElement);
}, 'Adding definition to scoped registry should not upgrade nodes in constructed documents');

test(t => {
  const name = nextCustomElementName();

  const iframe = createIFrameForTest(t);
  const registry = new CustomElementRegistry;
  const host = iframe.contentDocument.createElement('div');
  const shadow = host.attachShadow({mode: 'open', registry});
  const node = shadow.appendChild(iframe.contentDocument.createElement(name));
  iframe.contentDocument.body.appendChild(host);

  iframe.remove();

  class TestElement extends HTMLElement {};
  registry.define(name, TestElement);

  assert_false(node instanceof TestElement);
}, 'Adding definition to scoped registry should not upgrade nodes in detached frames');

promise_test(async t => {
  const name = nextCustomElementName();

  const newWindow = window.open('about:blank');
  t.add_cleanup(() => newWindow.close());

  const host = newWindow.document.createElement('div');
  const shadow = host.attachShadow({mode: 'open', registry: window.customElements});
  const node = shadow.appendChild(newWindow.document.createElement(name));
  newWindow.document.body.appendChild(host);

  newWindow.close();

  // `window.close()` is async. Wait a while until it's fully closed
  await new Promise(resolve => setTimeout(resolve, 500));

  class TestElement extends HTMLElement {};
  window.customElements.define(name, TestElement);

  assert_false(node instanceof TestElement);
}, 'Adding definition to scoped registry should not upgrade nodes in closed windows');

</script>

</body>
